import './style.scss'; import '@/app/styles/stock-common.scss'; import { cache } from 'react'; import { Metadata } from 'next'; import Link from 'next/link'; import { notFound } from 'next/navigation'; import { fetchStockDetail, fetchStockHistory } from '@/lib/api/stock'; import { fetchStockDisclosures } from '@/lib/api/market'; import { fetchStockDividends, fetchStockLending, fetchStockCbExercises } from '@/lib/api/seibro'; import { fetchStockBoardPosts } from '@/lib/api/community'; import { changeDirection, changeSymbol, formatChangeRate, formatKrwCompact, formatNumber, tradingStatusLabel } from '@/lib/utils/stock'; import Pager from '../_components/Pager'; import DisclosureList from '../../market/_components/DisclosureList'; import StockDiscussion from './_components/StockDiscussion'; import StockDividends from './_components/StockDividends'; import StockLending from './_components/StockLending'; import StockCbExercises from './_components/StockCbExercises'; import StockPriceChart from './_components/StockPriceChart'; import StockDetailHeader from './_components/StockDetailHeader'; const TABS = [ { value: 'price', label: '시세' }, { value: 'dividends', label: '배당' }, { value: 'lending', label: '대차' }, { value: 'cb', label: '전환권' }, { value: 'news', label: '뉴스' }, { value: 'disclosure', label: '공시' }, { value: 'discussion', label: '토론' } ]; const HISTORY_PER_PAGE = 30; const DISCUSSION_PER_PAGE = 20; const DIVIDENDS_PER_PAGE = 30; const LENDING_PER_PAGE = 90; const CB_PER_PAGE = 30; type Props = { params: Promise<{ code: string; }>; searchParams: Promise<{ tab?: string; page?: string; }>; }; // generateMetadata + 본문 이중 호출 방지 (요청 단위 dedupe) const getDetail = cache(async (code: string) => { return await fetchStockDetail(code); }); export async function generateMetadata({ params }: Props): Promise { const { code } = await params; const res = await getDetail(code); if (!res.success || !res.data) { return { title: '종목 상세 — 개미투자' }; } return { title: `${res.data.name}(${res.data.code}) 주가·시세 — 개미투자`, description: `${res.data.name}(${res.data.code}) 전일 종가·등락률·거래량·일별 시세 — 전일 종가 기준(T+1)` }; } export default async function StockDetailPage({ params, searchParams }: Props) { const { code } = await params; const query = await searchParams; const tab = TABS.some(c => c.value === query.tab) ? (query.tab as string) : 'price'; const page = Math.max(Number(query.page) || 1, 1); const res = await getDetail(code); // 없는 코드(형식 불일치 포함)는 404 if (!res.success && res.status === 404) { return notFound(); } // 백엔드 미기동 등 그 외 실패 — 에러 UI 로 우아하게 처리 if (!res.success || !res.data) { return (
종목 정보를 불러오지 못했습니다. 잠시 후 다시 시도해 주세요.
); } const detail = res.data; const latest = detail.recentPrices.length > 0 ? detail.recentPrices[0] : null; const direction = changeDirection(detail.changeRate); const statusLabel = tradingStatusLabel(detail.tradingStatus); // 시세 탭만 히스토리 조회 (request-time) const history = tab === 'price' ? await fetchStockHistory(code, { page, perPage: HISTORY_PER_PAGE }) : null; // 공시 탭 — 종목별 DART 공시 조회 (request-time) const disclosures = tab === 'disclosure' ? await fetchStockDisclosures(code, { page }) : null; // 토론 탭 — 종목 가상보드 글 목록 (request-time) const discussion = tab === 'discussion' ? await fetchStockBoardPosts(code, { page, perPage: DISCUSSION_PER_PAGE }) : null; // SEIBro 종목 탭 — 배당 이력 / 대차 시계열 / CB·BW 행사 (request-time) const dividends = tab === 'dividends' ? await fetchStockDividends(code, { page, perPage: DIVIDENDS_PER_PAGE }) : null; const lending = tab === 'lending' ? await fetchStockLending(code, { page, perPage: LENDING_PER_PAGE }) : null; const cbExercises = tab === 'cb' ? await fetchStockCbExercises(code, { page, perPage: CB_PER_PAGE }) : null; return (
{/* 상단 요약 — T+1 데이터임을 명시 */}

{detail.name}

{detail.code} {detail.market} {statusLabel && ( {statusLabel} )} {detail.sectorName && {detail.sectorName}}
{formatNumber(detail.closePrice)} {changeSymbol(direction)} {latest ? formatNumber(Math.abs(latest.changeAmount)) : '—'} ({formatChangeRate(detail.changeRate)})
거래량
{formatNumber(latest?.volume)}
거래대금
{formatKrwCompact(latest?.tradingValue)}
시가총액
{formatKrwCompact(detail.marketCap)}

전일 기준 · {detail.lastTradingDate ?? '데이터 준비 중'} 종가 (T+1 반영)

{/* 시세 그래프 — stock-detail__basis 바로 하단, 최근 종가 추이 (recentPrices) */} {/* 탭 */}
{tab === 'disclosure' ? ( !disclosures || !disclosures.success || !disclosures.data ? (
공시를 불러오지 못했습니다. 잠시 후 다시 시도해 주세요.
) : ( <> ) ) : tab === 'discussion' ? ( !discussion || !discussion.success || !discussion.data ? (
토론을 불러오지 못했습니다. 잠시 후 다시 시도해 주세요.
) : ( ) ) : tab === 'dividends' ? ( !dividends || !dividends.success || !dividends.data ? (
배당 이력을 불러오지 못했습니다. 잠시 후 다시 시도해 주세요.
) : ( <> ) ) : tab === 'lending' ? ( !lending || !lending.success || !lending.data ? (
대차 시계열을 불러오지 못했습니다. 잠시 후 다시 시도해 주세요.
) : ( <> ) ) : tab === 'cb' ? ( !cbExercises || !cbExercises.success || !cbExercises.data ? (
전환권 행사 내역을 불러오지 못했습니다. 잠시 후 다시 시도해 주세요.
) : ( <> ) ) : tab !== 'price' ? (

준비 중입니다.

) : !history || !history.success || !history.data ? (
일별 시세를 불러오지 못했습니다. 잠시 후 다시 시도해 주세요.
) : history.data.list.length === 0 ? (

수집된 일별 시세가 없습니다.

) : ( <> {history.data.list.map(row => { const rowDirection = changeDirection(row.changeRate); return ( ); })}
{detail.name} 일별 시세 — 날짜, 종가, 등락, 시가, 고가, 저가, 거래량, 거래대금
날짜 종가 등락률 시가 고가 저가 거래량 거래대금
{row.tradingDate} {formatNumber(row.close)} {changeSymbol(rowDirection)} {formatChangeRate(row.changeRate)} {formatNumber(row.open)} {formatNumber(row.high)} {formatNumber(row.low)} {formatNumber(row.volume)} {formatKrwCompact(row.tradingValue)}
)}
); }